/*
 *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package com.arashivision.webrtc;

import com.arashivision.webrtc.util.AsyncHttpURLConnection;
import com.arashivision.webrtc.util.AsyncHttpURLConnection.AsyncHttpEvents;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;

import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import io.socket.client.IO;
import io.socket.client.Manager;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import okhttp3.OkHttpClient;

/**
 * Negotiates signaling for chatting with https://appr.tc "rooms".
 * Uses the client<->server specifics of the apprtc AppEngine webapp.
 *
 * <p>To use: create an instance of this object (registering a message handler) and
 * call connectToRoom().  Once room connection is established
 * onConnectedToRoom() callback with room parameters is invoked.
 * Messages to other party (with local Ice candidates and answer SDP) can
 * be sent after WebSocket connection is established.
 */
public class WebSocketRTCClientNew implements AppRTCClient{
    private static final String TAG = "WebSocketRTCClientNew";
    private static final String ROOM_JOIN = "join";
    private static final String ROOM_MESSAGE = "message";
    private static final String ROOM_LEAVE = "leave";

//    private static final String CANDIDATE = "candidate";
//    private static final String JOIN = "join";
//    private static final String RECONNECT = "reconnect";
//    private static final String SDP = "sdp";
//    private static final String HANGUP = "hangup";

    private static final String SDP_OFFER = "offer";
    private static final String SDP_ANSWER = "answer";

    private enum ConnectionState { NEW, CONNECTED, CLOSED, ERROR }

    private enum MessageType { MESSAGE, LEAVE }

    private final Handler handler;
    private boolean initiator;
    private OneSignalingEvents events;
//    private WebSocketChannelClientNew wsClient;
    private ConnectionState roomState;
    private RoomConnectionParameters connectionParameters;
    private String messageUrl;
    private String leaveUrl;
    private Socket mSocket;

    private static final String ON_LINE_EVENT = "online";
    private static final String BROADCAST_EVENT = "broadcast";
    private static final String CONNET_EVENT = "connect";
    private static final String CONNETTING_EVENT = "connecting";
    private static final String DISCONNET_EVENT = "disconnect";
    private static final String DISCONNETING_EVENT = "disconnecting";
    private static final String ERROR_EVENT = "error";
    private static final String RECONNECT_EVENT = "reconnect";
    private static final String RECONNECT_ATTEMP_EVENT = "reconnectAttempt";
    private RTCConnectInfo mLocalInfo;
    private RemoteInfo mRemoteInfo;


    public WebSocketRTCClientNew(OneSignalingEvents events) {
        this.events = events;
        roomState = ConnectionState.NEW;
        final HandlerThread handlerThread = new HandlerThread(TAG);
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper());
    }

    // --------------------------------------------------------------------
    // AppRTCClient interface implementation.
    // Asynchronously connect to an AppRTC room URL using supplied connection
    // parameters, retrieves room parameters and connect to WebSocket server.
    @Override
    public void connectToRoom(RoomConnectionParameters connectionParameters) {
        this.connectionParameters = connectionParameters;
        handler.post(new Runnable() {
            @Override
            public void run() {
                connectToRoomInternal();
            }
        });
    }

    @Override
    public void connectToSignalServer(final RTCConnectInfo mConnectInfo) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                connectToSignalServerInternal(mConnectInfo);
            }
        });
    }

    @Override
    public void disconnectFromRoom() {
        handler.post(new Runnable() {
            @Override
            public void run() {
                disconnectFromRoomInternal();
                handler.getLooper().quit();
            }
        });
    }

    private void handleJoin(SocketActionInfo mSocketInfo)
    {
        Log.d(TAG,"handleJoin");
    }

    private void handleHangup(SocketActionInfo mSocketInfo)
    {
        events.onChannelClose();
    }

    private void handleReconnect(SocketActionInfo mSocketInfo)
    {
        Log.d(TAG,"handleReconnect");
    }

    private void closeSocket()
    {
        if(mSocket != null)
        {
            mSocket.disconnect();
            mSocket = null;
        }
        roomState = ConnectionState.CLOSED;
    }

    private void handleSocketInfo(JSONObject json)
    {
        SocketActionInfo mSocketActionInfo = SocketActionInfo.fromJson(json);

        if(mSocketActionInfo == null)
        {
            Log.e(TAG,"parse socket error " + json.toString());
            closeSocket();
            events.onChannelError(OneRTCConstants.ERROR.SOCKET_FORMAT_ERROR);
            return;
        }
        else if(mSocketActionInfo.isActionDeny())
        {
            Log.w(TAG,"action deny");
            closeSocket();
            events.onChannelError(OneRTCConstants.ERROR.ROOM_DENY);
            return;
        }
        else if(mSocketActionInfo.equalTarget(mSocket.id()))
        {
            Log.d(TAG,"handleSocketInfo " + json.toString());
            if (mSocketActionInfo.equalType(SocketActionInfo.PAYLOAD_CANDIDATE)) {
                events.onRemoteIceCandidate(mSocketActionInfo.getCandidate());
            }
            else if (mSocketActionInfo.equalType(SocketActionInfo.PAYLOAD_SDP)) {
                SessionDescription sdp = new SessionDescription(
                        SessionDescription.Type.fromCanonicalForm(mSocketActionInfo.getSDPType()),
                        mSocketActionInfo.getSDP());
                    if(mRemoteInfo != null)
                    {
                        if(mSocketActionInfo.equalClient(mRemoteInfo.socketId) && mSocketActionInfo.equalTarget(mSocket.id()))
                        {
                            events.onRemoteDescription(sdp);
                        }
                        else
                        {
                            Log.e(TAG,"client mismatch " + mRemoteInfo.socketId + " socket client " + mSocketActionInfo.getClient() );
                            Log.e(TAG,"target mismatch " + mSocket.id() + " socket target " + mSocketActionInfo.getTarget() );
                        }
                    }
                    else
                    {
                        Log.e(TAG," get sdp but no remote ");
                    }
            }
            else if (mSocketActionInfo.equalType(SocketActionInfo.PAYLOAD_HANGUP)) {
                handleHangup(mSocketActionInfo);
            }
            else if (mSocketActionInfo.equalType(SocketActionInfo.PAYLOAD_JOIN)) {
                handleJoin(mSocketActionInfo);
            }
            else if (mSocketActionInfo.equalType(SocketActionInfo.PAYLOAD_RECONNECT)) {
                handleReconnect(mSocketActionInfo);
            }
            else {
                reportError("Unexpected socket info type : " +
                        mSocketActionInfo.getPayloadType());
            }
        }
        else
        {
            reportError("mismatch socket id " + mSocket.id() +  " target " +
                    mSocketActionInfo.mMeta.target);
        }
    }

    private void handleOnlineInfo(String msg)
    {
        Log.i(TAG,ON_LINE_EVENT + " " + msg);
        OnLineInfo mOnLineInfo = new OnLineInfo();
        try {
            JSONObject json = new JSONObject(msg);
            JSONObject jsonClients = json.getJSONObject("clients");

            Iterator iterator = jsonClients.keys();
            mRemoteInfo = null;
            while(iterator.hasNext()){
                String socketId = (String) iterator.next();

                JSONObject client = jsonClients.getJSONObject(socketId);

                if(socketId.equals(mSocket.id()))
                {
                    Log.d(TAG," self " +  client);
                }
                else
                {
                    Log.d(TAG," remote " + client);
                    mRemoteInfo = new RemoteInfo();
                    mRemoteInfo.socketId = socketId;
                    mRemoteInfo.role = client.getString("role");
                    mRemoteInfo.userId = client.getString("userId");
                }
            }

            mOnLineInfo.action = json.getString("action");
            mOnLineInfo.message = json.getString("message");
            mOnLineInfo.target = json.optString("target");

            if(mRemoteInfo != null)
            {
                ConnectSignalServerInfo mConnectSignalInfo = new ConnectSignalServerInfo();

                mConnectSignalInfo.signalingParameters = new SignalingParameters(
                        // Ice servers are not needed for direct connections.
                        mLocalInfo.turnServers,
                        false, // Server side acts as the initiator on direct connections.
                        mSocket.id(), // clientId
                        null, // wssUrl
                        null, // wwsPostUrl
                        null, // offerSdp
                        null // iceCandidates
                );

                events.onConnectedSignalServer(mConnectSignalInfo);
            }
            else
            {
                Log.d(TAG," no remote ");
            }

        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    private void runOnSocketHandler(Runnable runnable)
    {
        if (Looper.myLooper() == handler.getLooper()) {
            runnable.run();
        } else {
            handler.post(runnable);
        }
    }

    private void connectToSignalServerInternal(RTCConnectInfo mConnectInfo)
    {
        roomState = ConnectionState.CONNECTED;

        SSLContext mySSLContext = null;
        TrustManager[] trustCerts = new TrustManager[] { new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

            }

            @Override
            public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

            }

            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        } };
            try {
                mySSLContext = SSLContext.getInstance( "TLS" );
                try {
                    mySSLContext.init( null, trustCerts, null );
                } catch (KeyManagementException e) {
                    e.printStackTrace();
                }
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .hostnameVerifier(new HostnameVerifier()
                    {
                        @Override
                        public boolean verify(String s, SSLSession sslSession) {
                            return true;
                        }
                    }).sslSocketFactory(mySSLContext.getSocketFactory(), new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                        }

                        @Override
                        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {

                        }

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[0];
                        }
                    }).build();
// default settings for all sockets
            IO.setDefaultOkHttpWebSocketFactory(okHttpClient);
            IO.setDefaultOkHttpCallFactory(okHttpClient);

// set as an option
            IO.Options opts = new IO.Options();
            opts.callFactory = okHttpClient;
            opts.webSocketFactory = okHttpClient;
            opts.forceNew = true;
            opts.reconnection = false;

            opts.query = "room=" + mConnectInfo.roomId + "&userId=" + mConnectInfo.userId
                                + "&role=" + mConnectInfo.role + "&transport=websocket";

            Log.d(TAG,"opts.query " + opts.query);
        try {
            mSocket = IO.socket(mConnectInfo.url,opts);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        mLocalInfo = mConnectInfo;
        mSocket.on(CONNET_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,CONNET_EVENT + mSocket.id());
                mSocket.on(mSocket.id(), new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        if(args.length > 0)
                        {
                            if(args[0] instanceof String)
                            {
                                String json = (String) args[0];
                                Log.d(TAG,"connect socket id  " + json + " mSocket id " + mSocket.id());
                                JSONObject obj = (JSONObject)args[1];
                                handleSocketInfo(obj);
                            }
                            else if (args[0] instanceof JSONObject)
                            {
                                JSONObject obj = (JSONObject)args[0];
                                handleSocketInfo(obj);
                            }
                            else
                            {
                                Log.e(TAG,"strange json len " + args.length);
                            }
                        }
                        else
                        {
                            Log.e(TAG,CONNET_EVENT + " arg len 0");
                        }
                    }
                });
            }
        });

        mSocket.on(ERROR_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,ERROR_EVENT + " " + args.length);
                if(args.length > 0)
                {
                    if(args[0] instanceof String)
                    {
                        events.onChannelError((String)args[0]);
                    }
                    else
                    {
                        Log.e(TAG,ERROR_EVENT + " arg0 not string ");
                    }
                }
                else
                {
                    events.onChannelError("unknown");
                }
            }
        });

        mSocket.on(DISCONNET_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,DISCONNET_EVENT);
//                events.onChannelClose();
            }
        });

        mSocket.on(ON_LINE_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args)
            {
                if(args.length > 0)
                {
                    if(args[0] instanceof String)
                    {
                        String json = (String) args[0];
                        Log.d(TAG,"1json to String " + json);
                        JSONObject obj = (JSONObject)args[1];
                        handleOnlineInfo(obj.toString());
                    }
                    else if (args[0] instanceof JSONObject)
                    {
                        JSONObject obj = (JSONObject)args[0];
                        handleOnlineInfo(obj.toString());
                    }
                    else
                    {
                        Log.e(TAG,"strange json len " + args.length);
                    }
                }
                else
                {
                    Log.e(TAG,CONNET_EVENT + " arg len 0");
                }
            }
        });

        mSocket.on(BROADCAST_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,BROADCAST_EVENT);
            }
        });

        mSocket.on(RECONNECT_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,RECONNECT_EVENT);
            }
        });

        mSocket.on(RECONNECT_ATTEMP_EVENT,new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                Log.d(TAG,RECONNECT_ATTEMP_EVENT);
            }
        });

//
//        mSocket.on("SomeOneOnline", new Emitter.Listener() {
//            @Override
//            public void call(Object... args) {
//                isOffer = true;
//                if (mPeer == null) {
//                    mPeer = new Peer();
//                }
//                mPeer.peerConnection.createOffer(mPeer, sdpConstraints);
//            }
//        }).on("IceInfo", new Emitter.Listener() {
//            @Override
//            public void call(Object... args) {
//                try {
//                    JSONObject jsonObject = new JSONObject(args[0].toString());
//                    IceCandidate candidate = null;
//                    candidate = new IceCandidate(
//                            jsonObject.getString("id"),
//                            jsonObject.getInt("label"),
//                            jsonObject.getString("candidate")
//                    );
//                    mPeer.peerConnection.addIceCandidate(candidate);
//                } catch (JSONException e) {
//                    e.printStackTrace();
//                }
//            }
//        }).on("SdpInfo", new Emitter.Listener() {
//            @Override
//            public void call(Object... args) {
//                if (mPeer == null) {
//                    mPeer = new Peer();
//                }
//                try {
//                    JSONObject jsonObject = new JSONObject(args[0].toString());
//                    SessionDescription description = new SessionDescription
//                            (SessionDescription.Type.fromCanonicalForm(jsonObject.getString("type")),
//                                    jsonObject.getString("description"));
//                    mPeer.peerConnection.setRemoteDescription(mPeer, description);
//                    if (!isOffer) {
//                        mPeer.peerConnection.createAnswer(mPeer, sdpConstraints);
//                    }
//                } catch (JSONException e) {
//                    e.printStackTrace();
//                }
//            }
//        });
        mSocket.connect();
    }

    // Connects to room - function runs on a local looper thread.
    private void connectToRoomInternal() {
    }

    // Disconnect from room and send bye messages - runs on a local looper thread.
    private void disconnectFromRoomInternal() {
        Log.d(TAG, "Disconnect. Room state: " + roomState);
        if (roomState == ConnectionState.CONNECTED) {
            Log.d(TAG, "Closing room.");
            sendHangup();
            closeSocket();
        }
        else
        {
            Log.e(TAG,"disconnect bad roomState " + roomState);
        }
    }

//    {"data":{"action":"exchange","payload":{"type":"hangup","role":"caller/callee","data":{"message":"Bye Bye"}}},"meta":{"timestamp":1514636791557,
//            "client":"\/webrtc#AXw27MYwJ_xe-Vh5AAJd",
//            "target":"\/webrtc#c-jodcfKqLWKShs7AAJh"}}

    private void sendHangup()
    {
        ExchangeData mExchange = new ExchangeData();

        Log.d(TAG,"sendHangup");
        mExchange.type = SocketActionInfo.PAYLOAD_HANGUP;
        mExchange.data = new JSONObject();

        jsonPut( mExchange.data,"message","Bye Bye");
        sendExchangeInfo(mExchange);
    }

    // Send local offer SDP to the other participant.
    @Override
    public void sendOfferSdp(final SessionDescription sdp) {
        Log.d(TAG,"sendOfferSdp");
        handler.post(new Runnable() {
            @Override
            public void run() {
            }
        });
    }

    private static class ExchangeData
    {
        public String type;
        public JSONObject data;
    }


    private void sendExchangeInfo(ExchangeData mData)
    {
        JSONObject json = new JSONObject();

//        Log.d(TAG,"mRemoteInfo.socketId " + mRemoteInfo.socketId);

        jsonPut(json,"target",mRemoteInfo.socketId);

        JSONObject jsonPayLoad = new JSONObject();
        jsonPut(jsonPayLoad,"role",mLocalInfo.role);
        jsonPut(jsonPayLoad,"type",mData.type);
        jsonPut(jsonPayLoad,"data",mData.data);

        jsonPut(json,"payload",jsonPayLoad);

        Log.d(TAG,"exchange " + mData.type + " " + json.toString());
        mSocket.emit("exchange",json);
    }

    // Send local answer SDP to the other participant.
    @Override
    public void sendAnswerSdp(final SessionDescription sdp) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                ExchangeData mMdata = new ExchangeData();

                mMdata.type = SocketActionInfo.PAYLOAD_SDP;
                mMdata.data = new JSONObject();

                jsonPut(mMdata.data,"type","answer");
                jsonPut(mMdata.data,"sdp",sdp.description);

                Log.d(TAG,"sdp type " + sdp.type + " desc " + sdp.description);
                sendExchangeInfo(mMdata);
            }
        });
    }

    // Send Ice candidate to the other participant.
    @Override
    public void sendLocalIceCandidate(final IceCandidate candidate) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                ExchangeData mMdata = new ExchangeData();

                mMdata.type = SocketActionInfo.PAYLOAD_CANDIDATE;
                mMdata.data = new JSONObject();
                jsonPut(mMdata.data,"candidate",candidate.sdp);
                jsonPut(mMdata.data,"sdpMid",candidate.sdpMid);
                jsonPut(mMdata.data,"sdpMLineIndex",candidate.sdpMLineIndex);


                sendExchangeInfo(mMdata);
            }
        });
    }

    // Send removed Ice candidates to the other participant.
    @Override
    public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
        Log.d(TAG,"sendLocalIceCandidateRemovals len " + candidates.length);
        handler.post(new Runnable() {
            @Override
            public void run() {

            }
        });
    }

    // --------------------------------------------------------------------
    // Helper functions.
    private void reportError(final String errorMessage) {
        Log.e(TAG, errorMessage);
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (roomState != ConnectionState.ERROR) {
                    roomState = ConnectionState.ERROR;
                    events.onChannelError(errorMessage);
                }
            }
        });
    }

    // Put a |key|->|value| mapping in |json|.
    private static void jsonPut(JSONObject json, String key, Object value) {
        try {
            json.put(key, value);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

}


